package com.iob.coin.blImpl.kgeditor;

import com.alibaba.fastjson.JSONObject;
import com.csvreader.CsvWriter;
import com.iob.coin.DAO.KGDAO;
import com.iob.coin.DAO.NodeDAO;
import com.iob.coin.DAO.TripleDAO;
import com.iob.coin.DAO.TypeDAO;
import com.iob.coin.Enums.OperateType;
import com.iob.coin.bl.kgeditor.KgEditorService;
import com.iob.coin.po.*;
import com.iob.coin.util.logutil.Logger;
import com.iob.coin.vo.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

/**
 * @Author: MengYuxin
 * @Date: 2021/3/12 21:52
 * 编辑节点和三元组
 */
@Service
@Transactional
public class KgEditorServiceImpl implements KgEditorService {

    @Autowired
    NodeDAO nodeDAO;

    @Autowired
    TripleDAO tripleDAO;

    @Autowired
    KGDAO kgDAO;

    @Autowired
    TypeDAO typeDAO;

    Logger logger = new Logger(this.getClass().getSimpleName(),System.out);

    /**
     * 维护一个历史记录的栈，由于每个kg都会有历史记录
     * 所以用hashmap来对应每一个kg的历史记录
     * key为kgId,value为对应kg的栈
     */
    private HashMap<Integer,Stack<Operate>>map=new HashMap<>();


    /**
     *
     * @param kgID  图谱的id
     * @param step  要撤回的步数
     * @return
     */
    @Override
    public ResponseVO Cancel(int kgID,int step) {
        if(map.containsKey(kgID)){
            Stack<Operate>operates=map.get(kgID);
            if(operates.empty()||operates.size()<step) return ResponseVO.buildFailure("没有更多的历史操作");
            else {
                for (int i=0;i<step;i++) {

                    Operate operate = operates.pop();

                    int op = operate.getOperateType().getValue();
                    /**
                     * AddNode(1),
                     * FixNode(2),
                     * DeleteNode(3),
                     * AddLink(4),
                     * FixLink(5),
                     * DeleteLink(6),
                     * AddType(7),
                     * FixType(8),
                     * DeleteType(9);
                     */
                    switch (op) {
                        case (1):
                            //添加节点撤回就需要删除节点
                            Node node1 = operate.getNode();
                            nodeDAO.deleteById(node1.getId());
                            break;
                        case (2):
                            //修改节点需要写入旧的节点的信息
                            Node node2 = operate.getNode();
                            System.out.println(JSONObject.toJSONString(node2));
                            nodeDAO.save(node2);
                            break;
                        case (3):
                            //删除节点撤回，要把删掉的节点和边都加上
                            Node node3 = operate.getNode();
                            System.out.println(node3.getId());
                            //nodeDAO.save(node3);
                            nodeDAO.insert_when_cancel(node3.getId(), node3.getKgID(),
                                    node3.getNodeAtId(), node3.getType_id(), node3.getLabel_ZH(),
                                    node3.getX(), node3.getY(), node3.getColor(), node3.getRadius());
                            if (operate.getTriples() != null) {
                                for (Triple tri : operate.getTriples()) {
                                    tripleDAO.insert_when_cancel(tri.getId(), tri.getKgID(),
                                            tri.getS_id(), tri.getO_id(), tri.getPredicate());
                                }
                            }
                            break;
                        case (4):
                            //添加连接撤回，就要删除连接
                            EditLinkVO editLinkVO1 = operate.getEditLinkVO();
                            int id = tripleDAO.TripleByKgIdAndS_idAndO_idAndPredicate(
                                    editLinkVO1.getKgID(), editLinkVO1.getS_id(),
                                    editLinkVO1.getO_id(), editLinkVO1.getPredicate()
                            ).get(0).getId();
                            tripleDAO.deleteById(id);
                            break;
                        case (5):
                            //修改连接撤回，就要重新写回被修改的连接
                            EditLinkVO editLinkVO2 = operate.getEditLinkVO();
                            Triple triple = new Triple(editLinkVO2.getId(), editLinkVO2.getS_id(),
                                    editLinkVO2.getO_id(), editLinkVO2.getPredicate(),
                                    editLinkVO2.getKgID());
                            tripleDAO.save(triple);
                            break;
                        case (6):
                            //删除连接撤回，重新添加连接
                            EditLinkVO e = operate.getEditLinkVO();
                            tripleDAO.insert_when_cancel(e.getId(), e.getKgID(),
                                    e.getS_id(), e.getO_id(), e.getPredicate());
                            break;
                        case (7):
                            //添加连接撤回，就要删除连接
                            TypeVO typeVO = operate.getTypeVO();
                            typeDAO.deleteByIdAndKgID(typeVO.getId(), typeVO.getKgID());
                            break;
                        case (8):
                            //修改连接撤回
                            TypeVO typeVO1 = operate.getTypeVO();
                            Type type1 = new Type(
                                    typeVO1.getId(), typeVO1.getKgID(), typeVO1.getType(),
                                    typeVO1.getColor(), typeVO1.getRadius()
                            );
                            typeDAO.save(type1);
                            break;
                        case (9):
                            /**
                             * 删除类型的撤回
                             * 先把删除的Type写会数据库
                             * 再把删除的与类型相关的Node和与Node相关的Triple重新写入数据库
                             */
                            List<Node> nodes = operate.getNodes();
                            List<Triple> triples = operate.getTriples();
                            TypeVO typeVO2 = operate.getTypeVO();
//                        Type type2=new Type(
//                                typeVO2.getId(),typeVO2.getKgID(),typeVO2.getType(),
//                                typeVO2.getColor(),typeVO2.getRadius()
//                        );
                            //恢复Type
                            typeDAO.insert_when_cancel(typeVO2.getId(), typeVO2.getKgID(),
                                    typeVO2.getType(), typeVO2.getColor(), typeVO2.getRadius());
                            //恢复Node
                            if (nodes != null) {
                                for (Node n : nodes) {
                                    nodeDAO.insert_when_cancel(n.getId(), n.getKgID(),
                                            n.getNodeAtId(), n.getType_id(), n.getLabel_ZH(),
                                            n.getX(), n.getY(), n.getColor(), n.getRadius());
                                }
                            }
                            //恢复Triple
                            if (triples != null) {
                                for (Triple t : triples) {
                                    tripleDAO.insert_when_cancel(t.getId(), t.getKgID(),
                                            t.getS_id(), t.getO_id(), t.getPredicate());
                                }
                            }
                            break;
                        default:
                            break;
                    }
                }

                logger.log("cancel","撤回操作",new HashMap<String,String>(){{
                    put("step",String.valueOf(step));
                    put("kgId",String.valueOf(kgID));
                }});

                return ResponseVO.buildSuccess();
            }

        }else {
            ResponseVO.buildFailure("没有对此图谱进行操作");
        }
        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO addNode(NodeVO nodeVO){
        //如果NodeAtId在Node表中已经存在，那么插入失败
        if(nodeDAO.findNodeByKgIdAndNodeAtId(nodeVO.getKgID(),nodeVO.getNodeAtId()).size()!=0) {
            return ResponseVO.buildFailure("节点URI已存在，不能重复插入相同URI节点");
        }
        /**
         * 如果对应的type在已经存在，使用现有的type_id
         * 如果不存在，那么就新建这个类型的type
         *
         int type_id=0;
         if(typeDAO.countType(nodeVO.getKgID(),nodeVO.getType())==0){
             Type type=new Type(nodeVO.getKgID(),nodeVO.getType(),nodeVO.getColor(),nodeVO.getRadius());
             type_id=typeDAO.save(type).getId();
         }else {
             type_id=typeDAO.findID(nodeVO.getType(),nodeVO.getKgID());
         }
            */
        //如果Node表中没有，插入这个节点
        String color="";
        double radius=-1;
        if(nodeVO.getColor()!=null){
            color=nodeVO.getColor();
        }

        Node node=new Node(nodeVO.getNodeAtId(),nodeVO.getType_id(),nodeVO.getLabel_ZH(),nodeVO.getKgID(),-1,-1,color,radius);
        Node savedNode = nodeDAO.save(node);
//        int id=nodeDAO.IdByNodeAtIdAndKgId(nodeVO.getNodeAtId(),nodeVO.getKgID());

        Operate operate=new Operate();

        operate.setOperateType(OperateType.AddNode);
        operate.setNode(node);

        //添加到历史记录里去
        if(map.containsKey(nodeVO.getKgID())){
            map.get(nodeVO.getKgID()).push(operate);

        }else {
            map.put(nodeVO.getKgID(),new Stack<Operate>());
            map.get(nodeVO.getKgID()).push(operate);
        }

        logger.log("addNode","添加节点",new HashMap<String,String>(){{
            put("info",String.valueOf(savedNode.toString()));
        }});

        //返回信息包含节点在数据库中的id

        return ResponseVO.buildSuccess(savedNode.getId());
    }

    @Override
    public ResponseVO fixNode(NodeVO nodeVO){
        //修改节点，如果节点在node表中不存在，那么返回失败信息
        if(nodeDAO.findById(nodeVO.getId()).size()==0){
            return ResponseVO.buildFailure("节点不存在");
        }
        List<Node>nodes=nodeDAO.findNodeByKgIdAndNodeAtId(nodeVO.getKgID(),nodeVO.getNodeAtId());
        if(nodes.size()!=0) {
            //不修改URI的情况，不进入这个if判断
            if (nodes.get(0).getId() != nodeVO.getId()) {
                return ResponseVO.buildFailure("节点URI已存在，不能将URI修改为和已存在的节点相同");
            }
        }
        Node old_node=nodeDAO.findById(nodeVO.getId()).get(0);//旧的节点信息
        System.out.println(JSONObject.toJSONString(old_node));
        Operate operate=new Operate();

        operate.setOperateType(OperateType.FixNode);
        operate.setNode(new Node(old_node.getId(),old_node.getNodeAtId(),
                old_node.getType_id(),old_node.getLabel_ZH(),old_node.getKgID(),
                old_node.getX(),old_node.getY(),old_node.getColor(),old_node.getRadius()));

        //添加到历史记录里去
        if(map.containsKey(nodeVO.getKgID())){
            map.get(nodeVO.getKgID()).push(operate);

        }else {
            map.put(nodeVO.getKgID(),new Stack<Operate>());
            map.get(nodeVO.getKgID()).push(operate);
        }

        Node node=new Node();
        node.setId(nodeVO.getId());
        node.setNodeAtId(nodeVO.getNodeAtId());
        node.setType_id(nodeVO.getType_id());
        //node.setNode_type(nodeVO.getNode_type());
        node.setLabel_ZH(nodeVO.getLabel_ZH());
        node.setKgID(nodeVO.getKgID());
        node.setX(nodeVO.getHor());
        node.setY(nodeVO.getVer());
        node.setColor(nodeVO.getColor());
        node.setRadius(nodeVO.getRadius());
        Node savedNode = nodeDAO.save(node);

        logger.log("fixNode","编辑节点",new HashMap<String,String>(){{
            put("info",String.valueOf(savedNode.toString()));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO deleteNode(int kgId,int id) {

        //根据图id和节点id
        //删除这个节点

        Node deletedNode = nodeDAO.findById(id).get(0);

        Operate operate=new Operate();
        operate.setOperateType(OperateType.DeleteNode);
        operate.setNode(deletedNode);

        if(tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId,id).size()!=0){
            //System.out.println("数量是"+tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId,id).size());
            //operate.setTriples(tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId,id));
            List<Triple>triples=tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId,id);
            operate.setTriples(triples);

            tripleDAO.deleteTripleWhenDeleteNode(kgId,id);
        }
        nodeDAO.deleteById(id);

        if(map.containsKey(kgId)){
            map.get(kgId).push(operate);
        }else {
            map.put(kgId,new Stack<Operate>());
            map.get(kgId).push(operate);
        }

        logger.log("deleteNode","删除节点",new HashMap<String,String>(){{
            put("info",String.valueOf(deletedNode.toString()));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO addType(TypeVO typeVO) {
        if(typeDAO.countType(typeVO.getKgID(),typeVO.getType())!=0){
            return ResponseVO.buildFailure("类型名称已存在，不能重复添加");
        }
        Type type=new Type(typeVO.getKgID(),typeVO.getType(),"",-1);
        Type savedType = typeDAO.save(type);
        Operate operate=new Operate();

        operate.setOperateType(OperateType.AddType);
        typeVO.setId(savedType.getId());
        operate.setTypeVO(typeVO);

        //添加到历史记录里去
        if(map.containsKey(typeVO.getKgID())){
            map.get(typeVO.getKgID()).push(operate);

        }else {
            map.put(typeVO.getKgID(),new Stack<Operate>());
            map.get(typeVO.getKgID()).push(operate);
        }

        logger.log("addType","添加类型",new HashMap<String,String>(){{
            put("info",String.valueOf(savedType.toString()));
        }});

        return ResponseVO.buildSuccess(savedType.getId());
    }

    @Override
    public ResponseVO fixType(TypeVO typeVO) {

        if(typeDAO.countType(typeVO.getKgID(),typeVO.getType())!=0){
           if(typeDAO.findID(typeVO.getType(),typeVO.getKgID())!=typeVO.getId()){
               ResponseVO.buildFailure("类型名称已存在，不能将类型名称修改为和已存在类型名称相同");
           }
        }
        Operate operate=new Operate();
        operate.setOperateType(OperateType.FixType);

        Type oldType=typeDAO.findById(typeVO.getId()).get(0);
        TypeVO oldTypeVO=new TypeVO(oldType.getId(),oldType.getKgID(),oldType.getNode_type(),
                oldType.getColor(),oldType.getRadius());
        operate.setTypeVO(oldTypeVO);

        //添加到历史记录里去
        if(map.containsKey(typeVO.getKgID())){
            map.get(typeVO.getKgID()).push(operate);

        }else {
            map.put(typeVO.getKgID(),new Stack<Operate>());
            map.get(typeVO.getKgID()).push(operate);
        }
        Type type=new Type(typeVO.getId(),typeVO.getKgID(),typeVO.getType(),typeVO.getColor(),typeVO.getRadius());
        Type savedType = typeDAO.save(type);

        logger.log("fixType","编辑类型",new HashMap<String,String>(){{
            put("info",String.valueOf(savedType.toString()));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO deleteType(int kgId, int id) {
        List<Node>nodes=nodeDAO.findNodeByKgIDAndType_id(kgId,id);
        List<Triple>triples=new ArrayList<>();

        //如果与Type相关的节点的个数不为0，逐个删除节点
        if(nodes.size()!=0) {
            for (Node node : nodes) {
                //如果如Node相关的连接的个数不为0，逐个删除连接(被删除的节点也连接都要保存到历史记录列表里去)
                if (tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId, node.getId()).size() != 0) {
                    List<Triple> tri = tripleDAO.TripleByKgIdAndS_idOr_O_id(kgId, node.getId());
                    triples.addAll(tri);
                    tripleDAO.deleteTripleWhenDeleteNode(kgId, node.getId());
                }
                nodeDAO.deleteById(node.getId());
            }
        }

        Operate operate=new Operate();
        operate.setOperateType(OperateType.DeleteType);
        operate.setNodes(nodes);
        operate.setTriples(triples);

        Type oldType = typeDAO.findById(id).get(0);
        TypeVO oldTypeVO = new TypeVO(oldType.getId(),oldType.getKgID(),oldType.getNode_type(),
                oldType.getColor(),oldType.getRadius());
        operate.setTypeVO(oldTypeVO);

        //添加到历史记录里去
        if(map.containsKey(kgId)){
            map.get(kgId).push(operate);

        }else {
            map.put(kgId,new Stack<Operate>());
            map.get(kgId).push(operate);
        }
        typeDAO.deleteByIdAndKgID(id,kgId);

        logger.log("deleteType","删除类型",new HashMap<String,String>(){{
            put("info",String.valueOf(oldType.toString()));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO addLink(EditLinkVO editLinkVO){
        int kgID=editLinkVO.getKgID();
        int s_id= editLinkVO.getS_id();
        int o_id= editLinkVO.getO_id();
        if(tripleDAO.TripleByKgIdAndS_idAndO_id(kgID,s_id,o_id).size()>10){
            return ResponseVO.buildFailure("两点之间的连接不能超过10个");
        }
        String predicate=editLinkVO.getPredicate();
        Triple triple=new Triple(s_id,predicate,o_id,kgID);
        Triple savedTriple = tripleDAO.save(triple);

        Operate operate=new Operate();
        editLinkVO.setId(savedTriple.getId());
        operate.setEditLinkVO(editLinkVO);
        operate.setOperateType(OperateType.AddLink);
        if(map.containsKey(editLinkVO.getKgID())){
            map.get(editLinkVO.getKgID()).push(operate);
        }else {
            map.put(editLinkVO.getKgID(),new Stack<Operate>());
            map.get(editLinkVO.getKgID()).push(operate);
        }

        logger.log("addLink","添加连接",new HashMap<String,String>(){{
            put("info",String.valueOf(savedTriple.toString()));
        }});

        //存入数据库后返回该三元组的id信息
        return ResponseVO.buildSuccess(savedTriple.getId());
    }

    @Override
    public ResponseVO deleteLink(int kgId,int id){
        Operate operate=new Operate();
        operate.setOperateType(OperateType.DeleteLink);
        Triple oldTri=tripleDAO.findById(id).get(0);
        EditLinkVO ed=new EditLinkVO(oldTri.getId(),oldTri.getS_id(),
                oldTri.getO_id(),oldTri.getKgID(),oldTri.getPredicate());
        //System.out.println(JSONObject.toJSONString(ed)+"  "+operate.getOperateType().value);
        operate.setEditLinkVO(ed);
        if(map.containsKey(kgId)){
            map.get(kgId).push(operate);
        }else {
            map.put(kgId,new Stack<Operate>());
            map.get(kgId).push(operate);
        }

        logger.log("deleteLink","删除连接",new HashMap<String,String>(){{
            put("info",String.valueOf(oldTri.toString()));
        }});

        tripleDAO.deleteById(id);
        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO fixLink(EditLinkVO editLinkVO){
        if(tripleDAO.findById(editLinkVO.getId()).size()==0){
            return ResponseVO.buildFailure("link不存在，请重试");
        }
        Operate operate=new Operate();
        operate.setOperateType(OperateType.FixLink);
        Triple oldTri=tripleDAO.findById(editLinkVO.getId()).get(0);
        operate.setEditLinkVO(new EditLinkVO(oldTri.getId(),oldTri.getS_id(),
                oldTri.getO_id(),oldTri.getKgID(),oldTri.getPredicate()));

        if(map.containsKey(editLinkVO.getKgID())){
            map.get(editLinkVO.getKgID()).push(operate);
        }else {
            map.put(editLinkVO.getKgID(),new Stack<Operate>());
            map.get(editLinkVO.getKgID()).push(operate);
        }

        int kgID=editLinkVO.getKgID();
        int s_id=editLinkVO.getS_id();
        int o_id=editLinkVO.getO_id();
        String predicate=editLinkVO.getPredicate();
        int id=editLinkVO.getId();//节点的id
        Triple triple=new Triple(id,s_id,o_id,predicate,kgID);
        Triple savedTriple = tripleDAO.save(triple);

        logger.log("fixLink","编辑连接",new HashMap<String,String>(){{
            put("info",String.valueOf(savedTriple.toString()));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO saveLayout(int kgId, List<NodeVO> nodeVOS) {
        for (NodeVO n:nodeVOS){
            //System.out.println(n.getHor());
            nodeDAO.saveLayout(n.getHor(),n.getVer(),kgId,n.getId());
        }

        logger.log("saveLayout","保存布局",new HashMap<String,String>(){{
            put("nodeCount",String.valueOf(nodeVOS.size()));
            put("kgId",String.valueOf(kgId));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO cancelLayout(int kgID) {
        nodeDAO.cancelLayout(kgID);

        logger.log("cancelLayout","撤销布局布局",new HashMap<String,String>(){{
            put("kgId",String.valueOf(kgID));
        }});

        return ResponseVO.buildSuccess();
    }

    @Override
    public ResponseVO outputFile(HttpServletResponse response, int kgId) {

        KG kg=kgDAO.kgById(kgId).get(0);
        int userId=kg.getUserId();

//        int userId=0;
//        if(kgDAO.kgById(kgId).size()!=0){
//            KG kg=kgDAO.kgById(kgId).get(0);
//            userId=kg.getUserId();
//        }

        String userPath = System.getProperty("user.dir") + "\\data\\" + userId + "\\";
        String path = userPath + "output" + kgId + ".csv";
        List<Triple>triples=tripleDAO.findByKgID(kgId);

        File file = null;
        CsvWriter csvWriter = null;
        try {
            file = new File(path);
            if(!file.exists()){
                file.createNewFile();
            }
            csvWriter = new CsvWriter(path, ',', Charset.forName("GBK"));
            String[] header={"atID","atType","predicate","objectID","label_ZH"};
            csvWriter.writeRecord(header);
            for(int i=0;i<triples.size();i++){
                Triple triple=triples.get(i);
                int s_id=triple.getS_id();
                int o_id=triple.getO_id();
                /**
                 * String[5]
                 * 对应"atID","atType","predicate","objectID","label_ZH"
                 */
                String[] info=new String[5];
                info[0]=nodeDAO.NodeAtIdByKgIdAndId(kgId,s_id);
                int type_id=nodeDAO.TypeByKgIdAndId(kgId,s_id);
                info[1]=typeDAO.getNodeType(type_id,kgId);
                info[2]=triple.getPredicate();
                info[3]=nodeDAO.NodeAtIdByKgIdAndId(kgId,o_id);
                info[4]=nodeDAO.LabelByKgIdAndId(kgId,o_id);
                csvWriter.writeRecord(info);
            }

            //todo 会导致异常 buildFailure("导出为文件失败") 但前端仍收到状态码200 返回成功
//            int a = 1/0;

        }catch (Exception e){
            e.printStackTrace();
            return ResponseVO.buildFailure("导出为文件失败");
        }finally {
            if(csvWriter != null) csvWriter.close();
        }

        // 写入二进制文件流
//        BufferedInputStream bis = null;
        FileInputStream bis = null;
//        BufferedOutputStream bos = null;
        ServletOutputStream bos = null;
        try{
            String simpleName = file.getName().substring(file.getName().lastIndexOf("/") + 1);
            String newFileName = new String(simpleName.getBytes(), StandardCharsets.UTF_8);
            response.setHeader("Content-disposition", "attachment;filename=" + newFileName);
            bis = new FileInputStream(file);
//            bos = new BufferedOutputStream(
//                    response.getOutputStream());
            bos = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseVO.buildFailure("导出为文件失败");
        } finally {
            if(bis != null)
                try { bis.close(); }
                catch (IOException e){
                    e.printStackTrace();
                } finally {
                    if (bos != null)
                        try { bos.close(); }
                        catch (IOException e) {
                            e.printStackTrace();
                        }
                        finally {
                            System.out.print("outputFile finished, try to delete " + path + ", result:");
                            System.out.println(file.delete());
                        }
                }
        }

        logger.log("outputFile","导出图谱文件",new HashMap<String,String>(){{
            put("kgId",String.valueOf(kgId));
        }});

        return ResponseVO.buildSuccess(file);
    }

}
